'''
CSV importer for the BCK animation type
CSVs that come from the j3d animation editor program
animations for now will be imported with linear interpolation
seems to me that the majority of the animations just use linear
as the interpolation method between keyframes of animation rows
'''

# Notes (AFAIK):
#
# - reference link --> https://wiki.cloudmodding.com/tww/BCK
# - In all the Mario CSV BCKs I've seen from J3D Animation
#   Editor the "smooth" interpolation type isn't used at all
#   for animation rows that contain more than the first frame value
#   it could be that JAE just converts the animation to linear
#   timing for easier processing but I am not certain

import bpy, math, re
from mathutils import Matrix
from .my_functions import *

#################################################
# read_csv_bck function (MAIN FUNCTION)
# read CSV file of BCK from J3D Animation Editor
# used to apply BCK animation to model on Blender
#################################################
def read_csv_bck(context, filepath, import_type):   
#  
  # this thing is always needed for stuff
  scene = bpy.context.scene
  
  # if nothing is selected end the exporter
  if (scene.objects.active == None
      or
      scene.objects.active.type != 'ARMATURE'):
    error_string = "No Armature object selected. Select one and try again."
    print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n")
    show_message(error_string, "Error exporting collada file", 'ERROR')
    return {'FINISHED'}
  
  # get armature object
  armature = scene.objects.active  
  print()
  print("###############")
  print("Armature found: %s" % armature.name)
  print()
  
  # deselect everything (to be sure nothing weird happens)
  bpy.ops.object.select_all(action='DESELECT')
  scene.objects.active = None
  
  # re-select the armature object only
  armature.select = True
  scene.objects.active = armature
  
  # open file to read
  f = open(filepath, 'r', encoding='utf-8')
  
  # check the file if it is from a BCK file and check the 
  # number of bones, if the file is not from a BCK 
  # or the bone count differs from the armature
  # bone count end the importer
  
  # first line has the animation type
  line = f.readline()
  
  if not (line.find("bck") + 1):
    error_string = "CSV is not for the BCK animation type. Check the file and try again."
    print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n")
    show_message(error_string, "Error importing CSV file data", 'ERROR')
    return {'FINISHED'}
    
  # count the bones of the animation on the CSV
  # store each time the "Scale X" is found in a line
  bone_count = 0
  while True:
  
    line = f.readline()
    
    # count the bones
    if (line.find("Scale X") + 1):
      bone_count = bone_count + 1
    
    # if an empty line or a line with a newline character 
    # is reached end of file must've been reached
    if (line == "" or line == '\n'):
      break
  
  # check bone count
  if (bone_count != len(armature.data.bones)):
    error_string = "Number of bones on the Armature is different than the number of animated bones on the CSV. Check the CSV file and try again."
    print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n")
    show_message(error_string, "Error importing CSV file data", 'ERROR')
    return {'FINISHED'}
  
  ###############
  # file is valid
  ###############
  
  # start extracting animation data  
  
  ###########################################################################
  ###########################################################################
  # import type "ignore rest pose"
  # in here I don't need to do any calculation of the data read from the CSV
  # the armature in which this animation is to be applied must've been 
  # imported before in the "ignore rest pose" mode (i.e. the child bones are
  # in the origin of the reference system defined by the parent's bone head).
  # This is like this because BCK CSVs contain the data of how a child bone
  # moves with respect to its parent. 
  ###########################################################################
  ###########################################################################
  if (import_type == True):
  #
    # read the CSV from the start again
    # to import the animation values
    
    f = open(filepath, 'r', encoding='utf-8') 
      
    line = f.readline()
    
    # first line of the CSV
    csv_first_line = line.split(',')
    animation_length = int(csv_first_line[1])
    
    # get the animation last frame and set it in blender
    bpy.data.scenes["Scene"].frame_end = int(csv_first_line[1])
    
    # the next line comes with the notable keyframes used in the animation
    line = f.readline()
    
    # get all keyframe numbers
    csv_keyframe_numbers = re.findall('[0-9]+', line)
    csv_keyframe_numbers = [int(string) for string in csv_keyframe_numbers]
    
    ###############################################################
    # in the next loop starts the bone animation data table reading
    # loop through every bone
    ###############################################################  
    for bone_number in range(bone_count):
      
      # get pose bone
      pose_bone = armature.pose.bones[bone_number]
      print("Reading/Writing animation for bone: %s" % (pose_bone.name))
      
      # set bone rotation mode to Extrinsic Euler XYZ
      pose_bone.rotation_mode = 'XYZ'
      
      # bone_anim_data will hold the bone keyframe animation
      # data for a single bone from the CSV file as follows
      
      #   row 1 --> X scaling
      #   row 2 --> Y scaling
      #   row 3 --> Z scaling
      #   row 4 --> X rotation
      #   row 5 --> Y rotation
      #   row 6 --> Z rotation
      #   row 7 --> X translation
      #   row 8 --> Y translation
      #   row 9 --> Z translation
      
      # already initialized with 9 rows as I don't need more than that
      bone_anim_data = [[], [], [], [], [], [], [], [], []]
      
      ###########################
      # read bone animation table
      ###########################
      for i in range(9):
      #      
        line = f.readline()
        row_data = line.split(',')
        
        # if row_data's length is not equal to the length of 
        # csv_keyframe_numbers + 3 fill it with empty strings to match said length
        while (len(row_data) != (len(csv_keyframe_numbers) + 3)):
          row_data.append("")
        
        ############################################################
        # copy the keyframe values from row_data into bone_anim_data
        ############################################################
        for j in range(len(row_data)):
        #        
          # first 3 elements of row_data are skipped
          # example: Joint 0, Scale X:, Linear, ...
          if (j < 3):
            continue
          
          # if there is no animation value for the bone on the
          # current keyframe data append None to said position
          # otherwise append the value 
          if (row_data[j] == "" or row_data[j] == '\n'):
            bone_anim_data[i].append(None)
          else:
            bone_anim_data[i].append(float(row_data[j]))
        #
      #
      
      ####################################
      # write bone animation table to bone
      # only go through the keyframes 
      # pointed to in csv_keyframe_numbers
      ####################################    
      for k in range(len(csv_keyframe_numbers)):
      #      
        # set frame on Blender
        scene.frame_set(csv_keyframe_numbers[k])
        
        # apply scale values X-Y-Z
        if (bone_anim_data[0][k] != None):
          pose_bone.scale[0] = bone_anim_data[0][k]
          pose_bone.keyframe_insert(data_path = 'scale', index = 0)
        if (bone_anim_data[1][k] != None):
          pose_bone.scale[1] = bone_anim_data[1][k]
          pose_bone.keyframe_insert(data_path = 'scale', index = 1)
        if (bone_anim_data[2][k] != None):
          pose_bone.scale[2] = bone_anim_data[2][k]
          pose_bone.keyframe_insert(data_path = 'scale', index = 2)
          
        # apply rotation values (converted to radians)
        if (bone_anim_data[3][k] != None):
          pose_bone.rotation_euler[0] = math.radians(bone_anim_data[3][k])
          pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 0)
        if (bone_anim_data[4][k] != None):
          pose_bone.rotation_euler[1] = math.radians(bone_anim_data[4][k])
          pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 1)
        if (bone_anim_data[5][k] != None):
          pose_bone.rotation_euler[2] = math.radians(bone_anim_data[5][k])
          pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 2)
        
        # apply translation values (divided by 100 because 1 GU is 100 meters)
        if (bone_anim_data[6][k] != None):
          pose_bone.location[0] = bone_anim_data[6][k] / 100
          pose_bone.keyframe_insert(data_path = 'location', index = 0)
        if (bone_anim_data[7][k] != None):
          pose_bone.location[1] = bone_anim_data[7][k] / 100
          pose_bone.keyframe_insert(data_path = 'location', index = 1)
        if (bone_anim_data[8][k] != None):
          pose_bone.location[2] = bone_anim_data[8][k] / 100
          pose_bone.keyframe_insert(data_path = 'location', index = 2)
      #
    #
  #
  
  ###########################################################################
  ###########################################################################
  # import type "rest pose"
  # in here I need to calculate the animation values for the reference system
  # defined by the bone's rest pose as the BCK CSVs store animation data of a
  # bone with respect to its parent. Conversion from a reference system to 
  # another can be done easily with matrix multiplication but the rotation 
  # data must be preprocessed first (angles that are outside the -180/+180
  # degree range are lost in the conversion process)
  ###########################################################################
  ###########################################################################
  else:
  #
    # read the CSV from the start again
    # to import the animation values
    
    f = open(filepath, 'r', encoding='utf-8') 
      
    line = f.readline()
    
    # first line of the CSV
    csv_first_line = line.split(',')
    animation_length = int(csv_first_line[1])
    
    # get the animation last frame and set it in blender
    bpy.data.scenes["Scene"].frame_end = int(csv_first_line[1])
    
    # the next line comes with the notable keyframes used in the animation
    line = f.readline()
    
    # get all keyframe numbers
    csv_keyframe_numbers = re.findall('[0-9]+', line)
    csv_keyframe_numbers = [int(string) for string in csv_keyframe_numbers]
    
    ###############################################################
    # in the next loop starts the bone animation data table reading
    # loop through every bone
    ###############################################################  
    for bone_number in range(bone_count):
      
      # get pose.bone and data.bone
      pose_bone = armature.pose.bones[bone_number]
      data_bone = armature.data.bones[bone_number]
      print("Reading/Writing animation for bone: %s" % (pose_bone.name))
      
      # set bone rotation mode to Extrinsic Euler XYZ
      pose_bone.rotation_mode = 'XYZ'
      
      # bone_anim_data will hold the bone frame animation (all frames)
      # data for a single bone from the CSV file as follows
      
      #   row 1 --> X scaling
      #   row 2 --> Y scaling
      #   row 3 --> Z scaling
      #   row 4 --> X rotation
      #   row 5 --> Y rotation
      #   row 6 --> Z rotation
      #   row 7 --> X translation
      #   row 8 --> Y translation
      #   row 9 --> Z translation
      
      # already initialized with 9 rows as I don't need more than that
      bone_anim_data = [[], [], [], [], [], [], [], [], []]
      
      ###########################
      # read bone animation table
      ###########################
      for i in range(9):
      #      
        line = f.readline()
        row_data = line.split(',')
        
        # if row_data's length is not equal to the length of 
        # csv_keyframe_numbers + 3 fill it with empty strings to match said length
        while (len(row_data) != (len(csv_keyframe_numbers) + 3)):
          row_data.append("")
        
        ############################################################
        # copy the keyframe values from row_data into bone_anim_data
        # and make bone_anim_data hold all frame values for the bone        
        ############################################################
        
        # k will be used to go through csv_keyframe_numbers
        # and also through row_data as its length is
        # (len(csv_keyframe_numbers) + 3)
        j = 0
        for k in range(animation_length + 1):
        #        
          # Note: first 3 elements of row_data are skipped
          #       example: Joint 0, Scale X:, Linear, ...
          
          # if the animation frame isn't a keyframe append None to
          # bone_anim_data[i] and go to the next frame
          if (k != csv_keyframe_numbers[j]):
            bone_anim_data[i].append(None)
            continue
          
          # if it is a keyframe advance to the next keyframe
          # on csv_keyframe_numbers
          
          # if there is no animation value for the bone on the
          # current keyframe data append None to said position
          # otherwise append the value 
          if (row_data[j + 3] == "" or row_data[j + 3] == '\n'):
            bone_anim_data[i].append(None)
          else:
            bone_anim_data[i].append(float(row_data[j + 3]))
            
          # advance in row_data
          j = j + 1
        #
      #
      
      ########################################################
      # modify bone_anim_data rotation animation values
      # so that the angles on it are between -180/+180
      # to avoid visual animation data loss
      # convert_rot_anim_to_180() will be an external function
      # the process of converting the animation rotation on all
      # axises into the -180/180 degree range can create new
      # keyframes to the aniamtion and they will be appended
      # at the end of csv_keyframe_numbers so that those 
      # keyframes are processed after the main ones are done
      ########################################################
      
      convert_rot_anim_to_180(bone_anim_data[3], csv_keyframe_numbers)
      convert_rot_anim_to_180(bone_anim_data[4], csv_keyframe_numbers)
      convert_rot_anim_to_180(bone_anim_data[5], csv_keyframe_numbers)
      
      ####################################
      # write bone animation table to bone
      # only go through the keyframes
      # pointed to in csv_keyframe_numbers
      ####################################
      
      # k is used to go through csv_keyframe_numbers
      # kf_num is used to go through bone_anim_data
      for k in range(len(csv_keyframe_numbers)):
      #
        # get keyframe number
        kf_num = csv_keyframe_numbers[k]      
        # set keyframe on Blender
        scene.frame_set(kf_num)
        
        # - get animation data
        # - convert animation to rest pose reference system
        
        # find all 9 animation properties with linear interpolation if needed 
        # (for now) the final goal is to only keep the keyframes from the BCK without
        # having to assign a keyframe for each frame of the animation in Blender
        
        # anim_temp will be used to hold the 9 animation
        # property data as it is read/calculated
        
        anim_temp = []
        
        ########################################################
        # find scale values in ignore rest pose reference system
        ########################################################
        
        #########
        # Scale X
        if (bone_anim_data[0][kf_num] != None):
          anim_temp.append(bone_anim_data[0][kf_num])
        else:          
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[0], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear"))
        
        #########
        # Scale Y
        if (bone_anim_data[1][kf_num] != None):
          anim_temp.append(bone_anim_data[1][kf_num])
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[1], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear"))
        
        #########
        # Scale Z
        if (bone_anim_data[2][kf_num] != None):
          anim_temp.append(bone_anim_data[2][kf_num])
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[2], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear"))
        
        ########################################################################
        # find rotation values in ignore rest pose reference system (in radians)
        ########################################################################
        
        ############
        # Rotation X
        if (bone_anim_data[3][kf_num] != None):
          anim_temp.append(math.radians(bone_anim_data[3][kf_num]))
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[3], kf_num)
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(math.radians(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear")))
        
        ############
        # Rotation Y
        if (bone_anim_data[4][kf_num] != None):
          anim_temp.append(math.radians(bone_anim_data[4][kf_num]))
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[4], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(math.radians(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear")))
        
        ############
        # Rotation Z
        if (bone_anim_data[5][kf_num] != None):
          anim_temp.append(math.radians(bone_anim_data[5][kf_num]))
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[5], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(math.radians(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear")))
          
        ##############################################################
        # find translation values in ignore rest pose reference system
        # divided by 100 because 1 GU is 100 meters
        ##############################################################
        
        ###############
        # Translation X
        if (bone_anim_data[6][kf_num] != None):
          anim_temp.append(bone_anim_data[6][kf_num] / 100)
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[6], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear") / 100)
        
        ###############
        # Translation Y
        if (bone_anim_data[7][kf_num] != None):
          anim_temp.append(bone_anim_data[7][kf_num] / 100)
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[7], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear") / 100)
        
        ###############
        # Translation Z
        if (bone_anim_data[8][kf_num] != None):
          anim_temp.append(bone_anim_data[8][kf_num] / 100)
        else:
          # find left and right values (store in temp array)
          a = find_left_right(bone_anim_data[8], kf_num)          
          # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
          anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear") / 100)
        
        ###################################################
        # create the animation matrix related to the ignore
        # rest pose (irp) transformation matrix (T * R * S)
        ###################################################
        
        anim_irp_mat =  calc_translation_matrix(anim_temp[6], anim_temp[7], anim_temp[8]) * calc_rotation_matrix(anim_temp[3], anim_temp[4], anim_temp[5]) * calc_scale_matrix(anim_temp[0], anim_temp[1], anim_temp[2])
        
        #########################################################
        # calculate the animation matrix related to the rest pose
        # rest pose matrix of the bone is in the data_bone        
        # the first bone has no parent
        #########################################################
        
        if (bone_number == 0):        
          anim_rp_mat = anim_irp_mat
        else:
          anim_rp_mat = (data_bone.parent.matrix_local.inverted() * data_bone.matrix_local).inverted() * anim_irp_mat
        
        #####################################
        # extract calculated animation values
        #####################################
        
        # Scaling      
        bone_scale = anim_rp_mat.to_scale()        
        # Rotation (Extrinsic Euler XYZ)
        bone_rotation = anim_rp_mat.to_euler('XYZ')        
        # Translation
        bone_translation = anim_rp_mat.to_translation()
        
        ####################################################
        # apply the respective keyframe values to the bone
        # if any of the values on scale/rotation/translation
        # is different than None create a keyframe for all
        # XYZ axises on said scale/rotation/translation
        # animation property. Will keep the individual
        # keyframe assignment just in case it is needed in
        # a future program logic update
        ####################################################
        
        ########################
        # apply scale values XYZ
        if (bone_anim_data[0][kf_num] != None
            or
            bone_anim_data[1][kf_num] != None
            or
            bone_anim_data[2][kf_num] != None):
          pose_bone.scale[0] = bone_scale[0]
          pose_bone.keyframe_insert(data_path = 'scale', index = 0)
          pose_bone.scale[1] = bone_scale[1]
          pose_bone.keyframe_insert(data_path = 'scale', index = 1)
          pose_bone.scale[2] = bone_scale[2]
          pose_bone.keyframe_insert(data_path = 'scale', index = 2)
        
        ###########################  
        # apply rotation values XYZ
        if (bone_anim_data[3][kf_num] != None
            or
            bone_anim_data[4][kf_num] != None
            or
            bone_anim_data[5][kf_num] != None):
          pose_bone.rotation_euler[0] = bone_rotation[0]
          pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 0)
          pose_bone.rotation_euler[1] = bone_rotation[1]
          pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 1)
          pose_bone.rotation_euler[2] = bone_rotation[2]
          pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 2)
        
        ##############################
        # apply translation values XYZ
        if (bone_anim_data[6][kf_num] != None
            or
            bone_anim_data[7][kf_num] != None
            or
            bone_anim_data[8][kf_num] != None):
          pose_bone.location[0] = bone_translation[0]
          pose_bone.keyframe_insert(data_path = 'location', index = 0)
          pose_bone.location[1] = bone_translation[1]
          pose_bone.keyframe_insert(data_path = 'location', index = 1)
          pose_bone.location[2] = bone_translation[2]
          pose_bone.keyframe_insert(data_path = 'location', index = 2)
      #
    #  
  #
  
  ######################################
  # make all interpolation curves linear
  # loop through each curve and through 
  # each of the curve's keyframe
  curves = bpy.context.active_object.animation_data.action.fcurves
  for curve in curves:
    for keyframe in curve.keyframe_points:        
      keyframe.interpolation = 'LINEAR'  
  
  # importer end
  return {'FINISHED'}  
#


#################################################
# Stuff down is for the menu appending
# of the importer to work plus some setting stuff
# comes from a Blender importer template
#################################################

from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator


class Import_CSV_BCK(Operator, ExportHelper):
#
    """Import a CSV file from J3D Animation Editor of the BCK animation type. Armature to which the animation must be applied must be the only armature in scene and must be the correct one for the animation"""
    bl_idname = "import_scene.csv_bck"
    bl_label = "Import CSV of BCK (from J3D Anim Editor)"

    filename_ext = ".csv"

    filter_glob = StringProperty(
            default="*.csv",
            options={'HIDDEN'},
            maxlen=255,
            )
    
    import_type = BoolProperty( name = "Ignore Rest Pose",
                                description = "Ignore all bone's rest poses and apply animations as SMG does. Modifies the bone's original rest pose.",
                                default = False,
                                  )
    
    def execute(self, context):
        return read_csv_bck(context, self.filepath, self.import_type)
#

def menu_import_csv_bck(self, context):
    self.layout.operator(Import_CSV_BCK.bl_idname, text="CSV of BCK (from J3D Animation Editor) (.csv)")

bpy.utils.register_class(Import_CSV_BCK)
bpy.types.INFO_MT_file_import.append(menu_import_csv_bck)

# test call
bpy.ops.import_scene.csv_bck('INVOKE_DEFAULT')


